<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>WebGPU Simple Triangle</title>
    <style>
        @import url(res/webgpu-lesson.css);
        html, body {
        margin: 0;       /* remove the default margin          */
        height: 100%;    /* make the html,body fill the page   */
        }
        canvas {
        display: block;  /* make the canvas act like a block   */
        width: 100%;     /* make the canvas fill its container */
        height: 100%;
        }
    </style>
</head>
<body>
    <canvas></canvas>
</body>
<script type="module">

async function main() {
    // WebGPU 是异步 API，所以需要使用 async/await 来处理
    const adapter = await navigator.gpu?.requestAdapter();
    const device = await adapter?.requestDevice();
    if (!device) {
        fail('need a browser that supports WebGPU');
        return;
    }

    const canvas = document.querySelector('canvas');    
    const context = canvas.getContext('webgpu');
    // 询问系统首选的画布格式是什么( rgba8unorm 或 bgra8unorm)。
    // 这其实并不重要，重要的是通过查询，可以让用户的系统以最快的速度运行。
    const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
    // 调用 configure 将format传入 webgpu 画布上下文。
    // 将device传入画布，从而将画布与创建的设备关联起来。
    context.configure({
        device,
        format: presentationFormat,
    });

    const module = device.createShaderModule({
        label: 'our hardcoded triangle shaders',
        code: `
            // Inter-stage 变量在顶点着色器和片段着色器之间发挥作用。
            // Inter-stage 变量最常用于在三角形内进行纹理坐标插值
            // 与 WebGPU 中的几乎所有功能一样，顶点着色器和片段着色器之间是通过索引连接的。
            // 对于 Inter-stage 变量，它们也是通过 location 索引进行连接。
            // 像素坐标由像素边缘指定。而提供给片段着色器的值则是像素的中心坐标。
            struct VertexOutput {
                // 该字段不是 Inter-stage 变量，它是一个内置变量(builtin)。
                // @builtin(position) 在顶点着色器和片段着色器中的意义是不同的。
                // 在顶点着色器中，@builtin(position) 是 GPU 绘制三角形/线/点所需的输出。
                // 在片段着色器中，@builtin(position) 是一个输入。它是片段着色器当前被要求计算颜色的像素坐标。
                @builtin(position) position: vec4f,
                @location(0) texCoord: vec2f,
            };

            @vertex fn vs(
                @builtin(vertex_index) vertexIndex : u32
            ) -> VertexOutput {
                let pos = array(
                    // 1st triangle
                    vec2f( 0.0,  0.0),  // center
                    vec2f( 1.0,  0.0),  // right, center
                    vec2f( 0.0,  1.0),  // center, top
            
                    // 2st triangle
                    vec2f( 0.0,  1.0),  // center, top
                    vec2f( 1.0,  0.0),  // right, center
                    vec2f( 1.0,  1.0),  // right, top
                );
                
                var output: VertexOutput;
                let xy = pos[vertexIndex]; 
                output.position = vec4f(xy, 0.0, 1.0);
                output.texCoord = xy; // 将位置值作为纹理坐标传递
                return output;
            }

            // 声明采样器和纹理
            @group(0) @binding(0) var ourSampler: sampler;
            @group(0) @binding(1) var ourTexture: texture_2d<f32>;

            @fragment fn fs(fsInput: VertexOutput) -> @location(0) vec4f {
                // textureSample()使用纹理坐标来采样纹理   
                return textureSample(ourTexture, ourSampler, fsInput.texCoord);
            }
        ` ,
    });

    const pipeline = device.createRenderPipeline({
        label: 'our hardcoded triangle pipeline',
        layout: 'auto',
        vertex: {
            module,
        },
        fragment: {
            module,
            targets: [{ format: presentationFormat }],
        },
    });

    const renderPassDescriptor = {
        label: 'our basic canvas renderPass',
        colorAttachments: [
            {
                clearValue: [0.3, 0.3, 0.3, 1],
                // load意思是将纹理的现有内容加载到 GPU 中
                loadOp: 'clear', // 指定在绘制前将纹理清除为clearValue
                // storeOp: 'store'表示存储绘制结果。也可儿童discard来丢弃绘制的结果
                storeOp: 'store', // 
            },
        ],
    };

    // 创建纹理数据
    const kTextureWidth = 5;
    const kTextureHeight = 7;
    const _ = [255, 0, 0, 255]; // red
    const y = [255, 255, 0, 255]; // yellow
    const b = [0, 0, 255, 255]; // blue
    //prettier-ignore
    const textureData = new Uint8Array([
        b, _, _, _, _,
        _, y, y, y, _,
        _, y, _, _, _,
        _, y, y, _, _,
        _, y, _, _, _,
        _, y, _, _, _,
        _, _, _, _, _,
    ].flat());

    // 创建纹理
    const texture = device.createTexture({
        label: 'our texture',
        size: [kTextureWidth, kTextureHeight],
        format: 'rgba8unorm',
        // 将此纹理绑定到一个绑定组 | 将纹理数据复制到纹理
        usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
    }); 

    // 将纹理数据复制到纹理
    device.queue.writeTexture({
        texture,
            //  5 * 4,
            // rowsPerImage: 7,
        }, 
        textureData, 
        {bytesPerRow: kTextureWidth * 4}, {
            width: kTextureWidth,
            height: kTextureHeight,
    });

    const sampler = device.createSampler();

    const bindGroup = device.createBindGroup({
        label: 'our texture bind group',
        layout: pipeline.getBindGroupLayout(0),
        entries: [
            {
                binding: 0,
                resource: sampler ,
            },
            {
                binding: 1,
                resource: texture.createView() ,
            },
        ],
    });

    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
    // WeakMap 对象是一组键/值对的集合，其中的键是弱引用的。其键必须是对象，而值可以是任意的。
    // WeakMap 对象中的键值对在垃圾回收机制运行时会被移除。
    const canvasToSizeMap = new WeakMap();

    function resizeCanvasToDisplaySize(canvas) {
        // Get the canvas's current display size
        let { width, height } = canvasToSizeMap.get(canvas) || canvas;

        // Make sure it's valid for WebGPU
        width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
        height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));

        // Only if the size is different, set the canvas size
        const needResize = canvas.width !== width || canvas.height !== height;
        if (needResize) {
            canvas.width = width;
            canvas.height = height;
        }
        return needResize;
    }
    
    function render() {
        resizeCanvasToDisplaySize(canvas);

        renderPassDescriptor.colorAttachments[0].view =
            context.getCurrentTexture().createView();

        const encoder = device.createCommandEncoder({ label: 'our encoder' });
        const pass = encoder.beginRenderPass(renderPassDescriptor);
        pass.setPipeline(pipeline);
        pass.setBindGroup(0, bindGroup);

        pass.draw(6);
        pass.end();

        const commandBuffer = encoder.finish();
        device.queue.submit([commandBuffer]);
    }

    // ResizeObserver 接口监视 Element 内容盒或边框盒或者 SVGElement 边界尺寸的变化。
    // ResizeObserver 避免了通过回调函数调整大小时，通常创建的无限回调循环和循环依赖项。
    const observer = new ResizeObserver(entries => {
        for (const entry of entries) {
            canvasToSizeMap.set(entry.target, {
                width: entry.contentBoxSize[0].inlineSize,
                height: entry.contentBoxSize[0].blockSize,
            });
        }
        render();
    });

    observer.observe(canvas);
}

function fail(msg) {
    // eslint-disable-next-line no-alert
    alert(msg);
}

main();

</script>

</html>
